Technote 1119Serial Port ApocryphaApple Developer Technical Support |
CONTENTSNotes for
Both APIs |
This Technote describes a number of problems often encountered by developers when dealing with serial ports under Mac OS. Most of this information is available from other sources, but those sources are obscure and commonly overlooked. Specifically, this Note describes the correct techniques for finding, opening, closing, and yielding serial ports under the classic serial API and the Open Transport serial API. In addition, this Note describes the theory and practice of the original and Open Transport serial port arbitrators. This Technote is directed at all Mac OS developers who use serial ports. |
Notes for Both APIsMac OS provides two APIs for accessing the serial port:
the classic serial API based on Device Manager
Open and Close on DemandA serial port is a non-sharable resource. If your application has the port open, no other application can open it. For this reason, you should always open and close the serial port on demand. For example, if your application only uses the serial port as part of its registration process, you open the port when you commence the registration and close the port immediately after you are done. YieldingYielding is the process by which a passive serial program can yield the serial port to an active serial program, and regain the serial port after the active serial program is done. For example, if you set Apple Remote Access (version 2.1 and lower) to wait for an incoming call, you can still make outgoing PPP connections using FreePPP. This is because the passive serial program (ARA) yields the serial port to the active serial program (FreePPP). When FreePPP closes the serial port, ARA will resume ownership and continue waiting for an incoming call. |
The classic serial architecture is based on Device Manager
'DRVR's
, as described in
Inside
Macintosh:Devices. This section describes the correct way to
find, open, close, and yield serial ports under the classic serial
architecture.
The correct way to find all the serial ports under Mac OS is to
use the Communications Resource Manager (CRM) routine
CRMSearch
(part of the Communications Toolbox).
Unfortunately, the book that documents the Communications Resource
Manager (Inside the Macintosh
Communications Toolbox) is not available in electronic form,
so it can be hard to find documentation for CRMSearch
.
The following sample is included to make up for this deficiency:
static void PrintInfoAboutAllSerialPorts(void) // Prints a list of all the serial ports on the // machine, along with their corresponding input // and output driver names, to stdout. Typically // you would use a routine like this to populate a // popup menu of the available serial ports. { CRMRec commRecord; CRMRecPtr thisCommRecord; CRMSerialPtr serialPtr; (void) InitCRM(); // First set up commRecord to specify that // we're interested in serial devices. commRecord.crmDeviceType = crmSerialDevice; commRecord.crmDeviceID = 0; // Now repeatedly call CRMSearch to iterate // through all the serial ports. thisCommRecord = &commRecord; do { thisCommRecord = (CRMRecPtr) CRMSearch( (CRMRecPtr) thisCommRecord ); if ( thisCommRecord != nil ) { // Once we a have a CRMRec for the serial port, // we must cast the crmAttributes field to // a CRMSerialPtr to access to serial-specific // fields about the port. serialPtr = (CRMSerialPtr) thisCommRecord->crmAttributes; // Print the information about the port. printf("We have a port called: "%#s"\n", *(serialPtr->name)); printf(" input driver named: "%#s"\n", *(serialPtr->inputDriverName)); printf(" output driver named: "%#s"\n", *(serialPtr->outputDriverName)); printf("\n"); // Now ensure that CRMSearch finds the next device. commRecord.crmDeviceID = thisCommRecord->crmDeviceID; } } while ( thisCommRecord != nil ); } |
NOTE: |
IMPORTANT: |
The correct way to open a serial port has been documented for many years as part of the ARA API document, currently available on the Mac OS SDK Developer CDs. However, this source is somewhat obscure (and the enclosed sample code is somewhat out of date), so the information is repeated here for your convenience.
The process is very easy to describe in English:
If a serial port arbitrator is installed, always call
OpenDriver
to open the serial port; otherwise, walk the unit table to determine whether the driver is already open, and open it only if it isn't.
This high-level algorithm is captured in the following routines for opening both the input and output serial drivers:
static OSErr OpenOneSerialDriver(ConstStr255Param driverName, short *refNum) // The one true way of opening a serial driver. This routine // tests whether a serial port arbitrator exists. If it does, // it relies on the SPA to do the right thing when OpenDriver is called. // If not, it uses the old mechanism, which is to walk the unit table // to see whether the driver is already in use by another program. { OSErr err; if ( SerialArbitrationExists() ) { err = OpenDriver(driverName, refNum); } else { if ( DriverIsOpen(driverName) ) { err = portInUse; } else { err = OpenDriver(driverName, refNum); } } return err; } static OSErr OpenSerialDrivers(ConstStr255Param inName, ConstStr255Param outName, SInt16 *inRefNum, SInt16 *outRefNum) // Opens both the input and output serial drivers, and returns their // refNums. Both refNums come back as an illegal value (0) if we // can't open either of the drivers. { OSErr err; err = OpenOneSerialDriver(outName, outRefNum); if (err == noErr) { err = OpenOneSerialDriver(inName, inRefNum); if (err != noErr) { (void) CloseDriver(*outRefNum); } } if (err != noErr) { *inRefNum = 0; *outRefNum = 0; } return err; } |
The code for determining whether a serial port arbitrator is installed is shown below:
enum { gestaltSerialPortArbitratorAttr = 'arb ', gestaltSerialPortArbitratorExists = 0 }; static Boolean SerialArbitrationExists(void) // Test Gestalt to see if serial arbitration exists // on this machine. { Boolean result; long response; result = ( Gestalt(gestaltSerialPortArbitratorAttr, &response) == noErr && (response & (1 << gestaltSerialPortArbitratorExists) != 0) != 0) ); return result; } |
DriverIsOpen
, which walks the unit table to see if the
driver serial driver is present and open. Remember that this routine
-- which is inherently evil because it accesses low memory globals --
is only used if a serial port arbitrator is not installed.
static Boolean DriverIsOpen(ConstStr255Param driverName) // Walks the unit table to determine whether the // given driver is marked as open in the table. // Returns false if the driver is closed, or does // not exist. { Boolean found; Boolean isOpen; short unit; DCtlHandle dceHandle; StringPtr namePtr; found = false; isOpen = false; unit = 0; while ( ! found && ( unit < LMGetUnitTableEntryCount() ) ) { // Get handle to a device control entry. GetDCtlEntry // takes a driver refNum, but we can convert between // a unit number and a driver refNum using bitwise not. dceHandle = GetDCtlEntry( ~unit ); if ( dceHandle != nil && (**dceHandle).dCtlDriver != nil ) { // If the driver is RAM based, dCtlDriver is a handle, // otherwise it's a pointer. We have to do some fancy // casting to handle each case. This would be so much // easier to read in Pascal )-: if ( ((**dceHandle).dCtlFlags & dRAMBasedMask) != 0 ) { namePtr = & (**((DRVRHeaderHandle) (**dceHandle).dCtlDriver)).drvrName[0]; } else { namePtr = & (*((DRVRHeaderPtr) (**dceHandle).dCtlDriver)).drvrName[0]; } // Now that we have a pointer to the driver name, compare // it to the name we're looking for. If we find it, // then we can test the flags to see whether it's open or // not. if ( EqualString(driverName, namePtr, false, true) ) { found = true; isOpen = ((**dceHandle).dCtlFlags & dOpenedMask) != 0; } } unit += 1; } return isOpen; } |
NOTE: |
If you successfully open a serial port, you should make sure to
close it again when you're done. You should always use
CloseDriver
to close a serial port. Remember to close
both the input and output drivers. The following code illustrates the
correct way to close the serial driver:
static OSErr CloseSerialDrivers(SInt16 inRefNum, SInt16 outRefNum) { OSErr err; (void) KillIO(inRefNum); err = CloseDriver(inRefNum); if (err == noErr) { (void) KillIO(outRefNum); (void) CloseDriver(outRefNum); } return err; } |
The following techniques are ways to ensure that you close the serial driver even if your application quits abnormally:
SetThreadTerminator
.
ExitToShell
.
NOTE: |
The classic serial architecture has very limited support for yielding the serial port. Apple Remote Access does this using a private API exported by the Link Tool Manager (part of ARA). This API was never published by Apple, and is not available to third parties.
If your application requires serial port yielding, you might want to investigate using the OT serial API.
Open Transport provides a second API for serial on Mac OS, one that has much in common with the network APIs provided by OT. In the current implementation of OT (version 1.3 at the time of writing), the OT serial API is implemented as a shim layered on top of the classic serial drivers. This fact is important because the way you use the OT serial API affects the availability of serial ports to the classic API, and vice versa.
Inside Macintosh: Open Transport contains a lot of background material that you might find useful.
If you are using the OT serial API, the correct way to find all
the installed serial ports is to repeatedly call
OTGetIndexedPort
looking for all ports of type
kOTSerialDevice
. The following sample demonstrates this
technique:
static OSStatus PrintSerialPortInfo(const OTPortRecord *portRecord) // Prints information about the port with the given portRecord. { Str255 userVisibleName; // OTGetUserPortNameFromPortRef is a little known routine // from <OpenTptConfig.h> that allows you to get a user // visible name for an Open Transport port. OTGetUserPortNameFromPortRef(portRecord->fRef, userVisibleName); printf("Found a serial port with port reference $%08lx:\n", portRecord->fRef); printf(" User visible name is "%#s".\n", userVisibleName); printf(" String to pass to OTCreateConfiguration is "%s".\n", portRecord->fPortName); printf(" Name of provider module is "%s".\n", portRecord->fModuleName); printf("\n"); return kOTNoError; } static OSStatus OTFindSerialPorts(void) // Lists all of the serial ports on the machine using Open Transport. { OSStatus err; Boolean portValid; SInt32 portIndex; OTPortRecord portRecord; UInt16 deviceType; // Start portIndex at 0 and call OTGetIndexedPort until // we find there are no more ports. portIndex = 0; err = kOTNoError; do { portValid = OTGetIndexedPort(&portRecord, portIndex); if (portValid) { // For each valid port, get the deviceType and, if // it's a serial port and not an alias, call PrintSerialPort // to dump out its information. Note that you don't want // to include aliases to the serial ports in the list, otherwise // a standard machine will have 3 serial ports, "serialA", "serialB" // and "serial". deviceType = OTGetDeviceTypeFromPortRef(portRecord.fRef); if (deviceType == kOTSerialDevice && (portRecord.fInfoFlags & kOTPortIsAlias) == 0) { err = PrintSerialPortInfo(&portRecord); } } portIndex += 1; } while ( portValid && err == kOTNoError); return err; }
Yielding
|
A Tale of Two ArbitratorsThe Serial Port Arbitrator is one of the least understood components of the Mac OS, partly because it is installed by Apple Remote Access and is not a core component of the system. This section explains why serial port arbitration is necessary, and the features of the two serial port arbitrators. The Original ProblemThe original Mac OS Device Manager architecture has an
interesting 'quirk' in that, once a driver is opened, any
further calls to On pre-MultiFinder Macintoshes, this was never a problem because only one program could be running at a time, and presumably it had control of the serial ports. However, with the advent of MultiFinder, multiple applications could be running simultaneously, and so the serial port ownership became an issue. The Original SolutionThe original solution was fairly easy: if the serial port is already open, it must be in use by another application, and hence you should not try to use it. While this requires serial applications to poke around in the unit table, it was a perfectly serviceable solution. The New ProblemThe new problem arose with the advent of Apple Remote Access. ARA has a mode in which it will passively sit in the background waiting for calls. However, users were annoyed by the fact that ARA was permanently using their serial port (and, more specifically, their modem), so they could not make outgoing calls without first turning off ARA's answer mode. This problem was hard to get around because of the
original solution. A well-behaved application looked in the
unit table, noticed that the serial driver was in use, and
did not even attempt to call The New SolutionThe solution to this new problem was twofold. First, the
rules were changed for developers. The new rule is the one
described above: if a serial port
arbitrator is installed, applications should ignore the unit
table and always call Second, ARA shipped with the Serial Port Arbitrator . The
Serial Port Arbitrator patches
The original Serial Port Arbitrator shipped as part of ARA 1.0. Its operation was intimately tied with the ARA Link Tool Manager. The Link Tool Manager API, which ARA uses to open a serial port in passive mode, was never publically documented. The Newer SolutionUnfortunately, in computers, stability is death, and this
is as true for ARA as it is for any other part of Mac OS.
Part of the plan for ARA 3.0 was to get rid of the Link Tool
Manager, and its associated Serial Port Arbitrator. However,
by the time ARA 3.0 became a reality, developers were used
to the Serial Port Arbitrator and were happily calling
So ARA 3.0 includes a new serial port arbitrator, the
OpenTpt Serial Arbitrator, which includes the serial port
arbitration functionality of the original Serial Port
Arbitrator. Like the original Serial Port Arbitrator, the
OpenTpt Serial Arbitrator patches
The Latest ProblemsAlas, Mac OS has still to achieve serial port arbitration nirvana. A number of serious deficiencies remain in the OpenTpt Serial Arbitrator:
This note will be revised as these problems are addressed. |
SummaryThe Mac OS serial port is a shared resource, and the true owner of this resource -- the user -- gets upset when their serial programs do not play well together. By following the guidelines outlined in this note, your program will correctly find all the serial ports on the machine, use those serial ports in the most co-operative way, and be adored by Macintosh users around the world! |
|
Thanks to Brian Bechtel, Peter Gontier, Bo3b Johnson, Matt Mora, Roger Pantos, Craig Prouse, and George Warner.